package warehouse;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import org.xml.sax.InputSource;

@WebService()
public class Warehouse {

    DocumentBuilderFactory docBuildFact;
    DocumentBuilder docBuild;
    TransformerFactory transFactory;
    Map<Integer,Item> allItems;
    File userDir, appDir, itemFile, settingsFile, transactionsFile, stylesheetFile;
    Map<String,String> settings;
    List<Transaction> transactions;
    boolean inited = false;

    // output example: <?xml version="1.0" encoding="UTF-8" standalone="no"?><list><item><name>Milk</name><id>1</id><price>5</price><quantity>100</quantity></item></list>

    @WebMethod(operationName = "getAllItems")
    public String getAllItems() {
        try {
            init();
            Document doc = itemsToXml(allItems);

            return xmlToString(doc);
        } catch (Throwable t) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            t.printStackTrace(pw);
            return "ERROR: " + t.getMessage() + "\n\n\n" + sw.toString();
        }
    }


    // input example: <order><id>3</id><quantity>9001</quantity></order>

    // output example: <?xml version="1.0" encoding="UTF-8" standalone="no"?><receipt><id>3</id><quantity>9001</quantity><totalPrice>900100</totalPrice></receipt>

    @WebMethod(operationName = "buyItem")
    public String buyItem(@WebParam(name = "order")
    String order) {
        try {
            init();
            // parse request
            InputSource is = new InputSource();
            is.setCharacterStream(new StringReader(order));
            Document doc = docBuild.parse(is);

            NodeList children = doc.getElementsByTagName("order").item(0).getChildNodes();
            Integer id = null, boughtQuantity = null;
            for (int i=0; i<children.getLength(); i++) {
                Node child = children.item(i);
                String childName = child.getNodeName();
                String childData = ((CharacterData) (child.getFirstChild())).getData();
                if ("id".equals(childName)) {
                    id = Integer.parseInt(childData);
                } else if ("quantity".equals(childName)) {
                    boughtQuantity = Integer.parseInt(childData);
                }
            }

            // check availability
            Item item = allItems.get(id);
            if (item == null) {
                return "ERROR: There's no item with ID " + id;
            }

            String itemName = item.getName();
            if (item.getQuantity() < boughtQuantity) {
                return "ERROR: There are only " + item.getQuantity() + " "
                        + itemName + "; cannot buy " + boughtQuantity + " "
                        + itemName;
            }

            // perform purchage
            item.setQuantity(item.getQuantity()-boughtQuantity);
            saveItems(allItems, itemFile);
            int balance = Integer.parseInt(settings.get("balance"));
            int itemPrice = item.getPrice();
            int cost = boughtQuantity*itemPrice;
            settings.put("balance",""+(balance+cost));
            saveSettings(settings, settingsFile);

            // log transaction
            Transaction trans = new Transaction(id, boughtQuantity, itemPrice, cost, now(), itemName);
            transactions.add(trans);
            saveTransactions(transactions, transactionsFile);

            // response
            doc = docBuild.newDocument();

            Element root = doc.createElement("receipt");
            doc.appendChild(root);

            Element idOut = doc.createElement("id");
            root.appendChild(idOut);
            idOut.appendChild(doc.createTextNode(""+id));

            Element quantityOut = doc.createElement("quantity");
            root.appendChild(quantityOut);
            quantityOut.appendChild(doc.createTextNode(""+boughtQuantity));

            Element totalPrice = doc.createElement("totalPrice");
            root.appendChild(totalPrice);
            totalPrice.appendChild(doc.createTextNode(""+cost));

            return xmlToString(doc);
        } catch (Throwable t) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            t.printStackTrace(pw);
            return "ERROR: " + t.getMessage() + "\n\n\n" + sw.toString();
        }
    }

    @WebMethod(operationName = "updateMonth")
    public String updateMonth() {
        try {
            init();
            int month = Integer.parseInt(settings.get("currentMonth"));
            int inflation = Integer.parseInt(settings.get("priceInflation"));
            settings.put("currentMonth", ""+(month+1));
            saveSettings(settings, settingsFile);
            for (Item i : allItems.values()) {
                i.setPrice((i.getPrice()*inflation)/100);
            }
            saveItems(allItems, itemFile);
            return null;
        } catch (Throwable t) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            t.printStackTrace(pw);
            return "ERROR: " + t.getMessage() + "\n\n\n" + sw.toString();
        }
    }
    
    // output example: <?xml version="1.0" encoding="UTF-8" standalone="no"?><currentMonth>2</currentMonth>
    
    @WebMethod(operationName = "getMonth")
    public String getMonth() {
        try {
            init();
            int month = Integer.parseInt(settings.get("currentMonth"));
            Document doc = docBuild.newDocument();

            Element root = doc.createElement("currentMonth");
            doc.appendChild(root);
            root.appendChild(doc.createTextNode(""+month));

            return xmlToString(doc);
        } catch (Throwable t) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            t.printStackTrace(pw);
            return "ERROR: " + t.getMessage() + "\n\n\n" + sw.toString();
        }
    }

    // input examples
    // create new item:      <add><id>4</id><name>Ketchup</name><price>68</price><quantity>2000</quantity></add>
    // add to existing item: <add><id>4</id><quantity>2000</quantity></add>
    
    @WebMethod(operationName = "addItem")
    public String addItem(@WebParam(name = "addItemXML")
    String addItemXML) {
        try {
            init();
            // parse request
            InputSource is = new InputSource();
            is.setCharacterStream(new StringReader(addItemXML));
            Document doc = docBuild.parse(is);

            NodeList children = doc.getElementsByTagName("add").item(0).getChildNodes();
            String name = null;
            Integer id = null, price = null, quantity = null;
            for (int i=0; i<children.getLength(); i++) {
                Node child = children.item(i);
                String childName = child.getNodeName();
                String childData = ((CharacterData) (child.getFirstChild())).getData();
                if ("name".equals(childName)) {
                    name = childData;
                } else if ("id".equals(childName)) {
                    id = Integer.parseInt(childData);
                } else if ("price".equals(childName)) {
                    price = Integer.parseInt(childData);
                } else if ("quantity".equals(childName)) {
                    quantity = Integer.parseInt(childData);
                }
            }
            
            // assertions
            if (id == null) {
                return "ERROR: ID is null";
            }
            if (quantity == null || quantity < 1) {
                return "ERROR: Quantity is " + quantity + ", must be 1 or greater";
            }
            

            // perform addition
            Item item = allItems.get(id);
            if (item == null) {
                // new item
                allItems.put(id, new Item(name, id, price, quantity));
            } else {
                // existing item
                item.setQuantity(item.getQuantity()+quantity);
            }
            saveItems(allItems, itemFile);
            
            return null;
        } catch (Throwable t) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            t.printStackTrace(pw);
            return "ERROR: " + t.getMessage() + "\n\n\n" + sw.toString();
        }
    }
    
    // input examples
    // remove some: <remove><id>3</id><quantity>23</quantity></remove>
    // remove all:  <remove><id>3</id></remove>

    @WebMethod(operationName = "removeItem")
    public String removeItem(@WebParam(name = "removeItemXML")
    String removeItemXML) {
        try {
            init();
            // parse request
            InputSource is = new InputSource();
            is.setCharacterStream(new StringReader(removeItemXML));
            Document doc = docBuild.parse(is);

            NodeList children = doc.getElementsByTagName("remove").item(0).getChildNodes();
            Integer id = null, quantity = null;
            for (int i=0; i<children.getLength(); i++) {
                Node child = children.item(i);
                String childName = child.getNodeName();
                String childData = ((CharacterData) (child.getFirstChild())).getData();
                if ("id".equals(childName)) {
                    id = Integer.parseInt(childData);
                } else if ("quantity".equals(childName)) {
                    quantity = Integer.parseInt(childData);
                }
            }

            // check availability
            Item item = allItems.get(id);
            if (item == null) {
                return "ERROR: There's no item with ID " + id;
            }
            
            if (quantity != null && quantity < 1) {
                return "ERROR: The quantity is " + quantity
                        + ". Must be either at least 1 or omitted.";
            }
            
            // perform removal
            if (quantity == null) {
                // remove all          
                allItems.remove(id);
            } else {
                // remove a certain amount
                item.setQuantity(item.getQuantity() - quantity);
            }
            saveItems(allItems, itemFile);
            
            return null;
        } catch (Throwable t) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            t.printStackTrace(pw);
            return "ERROR: " + t.getMessage() + "\n\n\n" + sw.toString();
        }
    }

    private void init() throws Exception {
        if (!inited) {
            docBuildFact = DocumentBuilderFactory.newInstance();
            docBuild = docBuildFact.newDocumentBuilder();
            transFactory = TransformerFactory.newInstance();

            userDir = new File(System.getProperty("user.home"));
            appDir = new File(userDir, "webbtjanster/");
            itemFile = new File(appDir, "items.xml");
            settingsFile = new File(appDir, "settings.xml");
            transactionsFile = new File(appDir, "transactions.xml");
            stylesheetFile = new File(appDir, "transactionsStylesheet.xsl");
        }
        // default settings
        settings = new HashMap<String,String>();
        settings.put("currentMonth","0");
        settings.put("priceInflation","95");
        settings.put("balance","0");
        settings.put("timeZone","Europe/Stockholm");
        settings.put("locale","sv_SE");
        if (settingsFile.exists()) {
            settings.putAll(loadSettings(settingsFile));
        }
        saveSettings(settings, settingsFile);
        if (itemFile.exists()) {
            allItems = loadItems(itemFile);
        } else {
            // create some test data
            allItems = new HashMap<Integer,Item>();
            allItems.put(1, new Item("Milk", 1, 100, 900));
            allItems.put(2, new Item("Cookie", 2, 250, 500));
            allItems.put(3, new Item("Apple", 3, 120, 750));
            saveItems(allItems, itemFile);
        }
        if (transactionsFile.exists()) {
            transactions = loadTransactions(transactionsFile);
        } else {
            transactions = new LinkedList();
            saveTransactions(transactions, transactionsFile);
        }
        if (!stylesheetFile.exists()) {
            placeStylesheet();
        }
        inited = true;
    }

    private Map<Integer,Item> loadItems(File file) throws Exception {
        Map<Integer,Item> items;
        FileReader reader = null;
        try {
            InputSource is = new InputSource();
            reader = new FileReader(file);
            is.setCharacterStream(reader);
            Document doc = docBuild.parse(is);
            reader.close();
            items = xmlToItems(doc);
        } catch (Exception ex) {
            if (reader != null) {
                reader.close();
            }
            throw ex;
        }
        return items;
    }

    private void saveItems(Map<Integer,Item> items, File file) throws Exception {
        FileWriter writer = null;
        try {
            Document doc = itemsToXml(items);
            file.getParentFile().mkdirs();
            file.createNewFile();
            writer = new FileWriter(file);
            writer.write(xmlToString(doc));
            writer.close();
        } catch (Exception ex) {
            if (writer != null) {
                writer.close();
            }
            throw ex;
        }
    }

    private String xmlToString(Document doc) throws Exception {
        StringWriter buffer = new StringWriter();
        Transformer transformer = transFactory.newTransformer();
        transformer.transform(new DOMSource(doc), new StreamResult(buffer));
        return buffer.toString();
    }

    private Document itemsToXml(Map<Integer,Item> items) {
        Document doc = docBuild.newDocument();
        Element root = doc.createElement("list");
        doc.appendChild(root);

        for (Item i : items.values()) {
            Element item = doc.createElement("item");
            root.appendChild(item);

            Element name = doc.createElement("name");
            item.appendChild(name);
            name.appendChild(doc.createTextNode(i.getName()));

            Element id = doc.createElement("id");
            item.appendChild(id);
            id.appendChild(doc.createTextNode(Integer.toString(i.getId())));

            Element price = doc.createElement("price");
            item.appendChild(price);
            price.appendChild(doc.createTextNode(Integer.toString(i.getPrice())));

            Element quantity = doc.createElement("quantity");
            item.appendChild(quantity);
            quantity.appendChild(doc.createTextNode(Integer.toString(i.getQuantity())));
        }

        return doc;
    }

    private Map<Integer,Item> xmlToItems(Document doc) throws Exception {
        Map<Integer,Item> items = new HashMap<Integer,Item>();

        NodeList nodes = doc.getElementsByTagName("item");
        for (int i=0; i<nodes.getLength(); i++) {
            NodeList children = nodes.item(i).getChildNodes();
            String name = null;
            Integer id = null, price = null, quantity = null;
            for (int j=0; j<children.getLength(); j++) {
                Node child = children.item(j);
                String childName = child.getNodeName();
                String childData = ((CharacterData) (child.getFirstChild())).getData();
                if ("name".equals(childName)) {
                    name = childData;
                } else if ("id".equals(childName)) {
                    id = Integer.parseInt(childData);
                } else if ("price".equals(childName)) {
                    price = Integer.parseInt(childData);
                } else if ("quantity".equals(childName)) {
                    quantity = Integer.parseInt(childData);
                }
            }
            items.put(id, new Item(name, id, price, quantity));
        }
        
        return items;
    }

    private Map<String, String> loadSettings(File settingsFile) throws Exception {
        Map<String,String> set;
        FileReader reader = null;
        try {
            InputSource is = new InputSource();
            reader = new FileReader(settingsFile);
            is.setCharacterStream(reader);
            Document doc = docBuild.parse(is);
            reader.close();
            set = xmlToSettings(doc);
        } catch (Exception ex) {
            if (reader != null) {
                reader.close();
            }
            throw ex;
        }
        return set;
    }

    private void saveSettings(Map<String, String> settings, File settingsFile) throws Exception {
        FileWriter writer = null;
        try {
            Document doc = settingsToXml(settings);
            settingsFile.getParentFile().mkdirs();
            settingsFile.createNewFile();
            writer = new FileWriter(settingsFile);
            writer.write(xmlToString(doc));
            writer.close();
        } catch (Exception ex) {
            if (writer != null) {
                writer.close();
            }
            throw ex;
        }
    }

    private Map<String, String> xmlToSettings(Document doc) throws Exception {
        Map<String,String> set = new HashMap<String,String>();

        NodeList children = doc.getElementsByTagName("settings").item(0).getChildNodes();
        for (int i=0; i<children.getLength(); i++) {
            Node child = children.item(i);
            String childName = child.getNodeName();
            String childData = ((CharacterData) (child.getFirstChild())).getData();
            set.put(childName, childData);
        }
        
        return set;
    }

    private Document settingsToXml(Map<String, String> settings) {
        Document doc = docBuild.newDocument();
        Element root = doc.createElement("settings");
        doc.appendChild(root);

        for (String key : settings.keySet()) {
            String value = settings.get(key);
            Element e = doc.createElement(key);
            root.appendChild(e);
            e.appendChild(doc.createTextNode(value));
        }

        return doc;
    }

    private TimeZone timeZone() {
        return TimeZone.getTimeZone(settings.get("timeZone"));
    }

    private Locale locale() {
        String localeString = settings.get("locale");
        Locale loc = null;
        for (Locale l : Locale.getAvailableLocales()) {
            if (localeString.equals(l.toString())) {
                loc = l;
                break;
            }
        }
        return loc;
    }

    private Date now() {
        return Calendar.getInstance(timeZone(), locale()).getTime();
    }

    private List<Transaction> loadTransactions(File transactionsFile) throws Exception {
        List<Transaction> list;
        FileReader reader = null;
        try {
            InputSource is = new InputSource();
            reader = new FileReader(transactionsFile);
            is.setCharacterStream(reader);
            Document doc = docBuild.parse(is);
            reader.close();
            list = xmlToTransactions(doc);
        } catch (Exception ex) {
            if (reader != null) {
                reader.close();
            }
            throw ex;
        }
        return list;
    }

    private void saveTransactions(List<Transaction> transactions, File transactionsFile) throws Exception {
        FileWriter writer = null;
        try {
            Document doc = transactionsToXml(transactions);
            settingsFile.getParentFile().mkdirs();
            settingsFile.createNewFile();
            writer = new FileWriter(transactionsFile);
            writer.write(xmlToString(doc));
            writer.close();
        } catch (Exception ex) {
            if (writer != null) {
                writer.close();
            }
            throw ex;
        }
    }

    private List<Transaction> xmlToTransactions(Document doc) throws ParseException {
        List<Transaction> list = new LinkedList<Transaction>();

        NodeList transElements = doc.getElementsByTagName("transaction");
        for (int i=0; i<transElements.getLength(); i++) {
            NodeList children = transElements.item(i).getChildNodes();
            Integer id = null, quantity = null, itemPrice = null, totalPrice = null;
            String itemName = null;
            Date dateTime = null;
            for (int j=0; j<children.getLength(); j++) {
                Node child = children.item(j);
                String childName = child.getNodeName();
                String childData = ((CharacterData) (child.getFirstChild())).getData();
                if ("itemID".equals(childName)) {
                    id = Integer.parseInt(childData);
                } else if ("itemPrice".equals(childName)) {
                    itemPrice = Integer.parseInt(childData);
                } else if ("totalPrice".equals(childName)) {
                    totalPrice = Integer.parseInt(childData);
                } else if ("quantity".equals(childName)) {
                    quantity = Integer.parseInt(childData);
                } else if ("dateTime".equals(childName)) {
                    dateTime = stringToDate(childData);
                } else if ("itemName".equals(childName)) {
                    itemName = childData;
                }
            }
            list.add(new Transaction(id, quantity, itemPrice, totalPrice, dateTime, itemName));
        }

        return list;
    }

    private Document transactionsToXml(List<Transaction> transactions) {
        Document doc = docBuild.newDocument();
        Element root = doc.createElement("list");
        doc.appendChild(root);
        ProcessingInstruction pi = doc.createProcessingInstruction("xml-stylesheet",
                "type=\"text/xsl\" href=\"transactionsStylesheet.xsl\"");
        doc.insertBefore(pi, root);

        for (Transaction t : transactions) {
            Element trans = doc.createElement("transaction");
            root.appendChild(trans);

            Element id = doc.createElement("itemID");
            trans.appendChild(id);
            id.appendChild(doc.createTextNode(Integer.toString(t.getId())));

            Element quantity = doc.createElement("quantity");
            trans.appendChild(quantity);
            quantity.appendChild(doc.createTextNode(Integer.toString(t.getQuantity())));

            Element itemPrice = doc.createElement("itemPrice");
            trans.appendChild(itemPrice);
            itemPrice.appendChild(doc.createTextNode(Integer.toString(t.getItemPrice())));

            Element totalPrice = doc.createElement("totalPrice");
            trans.appendChild(totalPrice);
            totalPrice.appendChild(doc.createTextNode(Integer.toString(t.getTotalPrice())));

            Element dateTime = doc.createElement("dateTime");
            trans.appendChild(dateTime);
            dateTime.appendChild(doc.createTextNode(dateToString(t.getDateTime())));

            Element itemName = doc.createElement("itemName");
            trans.appendChild(itemName);
            itemName.appendChild(doc.createTextNode(t.getItemName()));
        }

        return doc;
    }

    private DateFormat dateFormat() {
        return DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, locale());
    }

    private Date stringToDate(String str) throws ParseException {
        return dateFormat().parse(str);
    }

    private String dateToString(Date dateTime) {
        return dateFormat().format(dateTime);
    }

    private void placeStylesheet() throws Exception {
        FileOutputStream os = null;
        InputStream is = null;
        try {
            stylesheetFile.getParentFile().mkdirs();
            stylesheetFile.createNewFile();
            os = new FileOutputStream(stylesheetFile);
            is = this.getClass().getResourceAsStream("transactionsStylesheet.xsl");
            int read = 0;
            byte[] bytes = new byte[1024];
            while ((read = is.read(bytes)) != -1) {
                os.write(bytes, 0, read);
            }
            os.close();
        } catch (Exception ex) {
            if (os != null) {
                os.close();
            }
            if (is != null) {
                is.close();
            }
            throw ex;
        }
    }

}
